/**
 * Copyright 2009 Sergio Bossa
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.googlecode.actorom.impl.core;

import com.googlecode.actorom.Address;
import com.googlecode.actorom.ExitActorMessage;
import com.googlecode.actorom.KillActorException;
import com.googlecode.actorom.RestartType;
import com.googlecode.actorom.Topology;
import com.googlecode.actorom.annotation.AddressInstance;
import com.googlecode.actorom.annotation.OnKill;
import com.googlecode.actorom.annotation.OnMessage;
import com.googlecode.actorom.annotation.OnRestart;
import com.googlecode.actorom.annotation.OnSpawn;
import com.googlecode.actorom.annotation.OnSwap;
import com.googlecode.actorom.annotation.Restart;
import com.googlecode.actorom.annotation.TopologyInstance;
import com.googlecode.actorom.annotation.TrapExit;
import com.googlecode.actorom.support.ActorExecutionException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import net.jcip.annotations.NotThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author Sergio Bossa
 */
@NotThreadSafe
public class CoreHandler {

    private static final transient Logger LOG = LoggerFactory.getLogger(CoreHandler.class);
    private final Map<Class, Method> onMessageMethods = new HashMap<Class, Method>();
    private final Object handler;

    public CoreHandler(Topology topology, Address address, Object handler) {
        this.handler = handler;
        injectTopologyInstance(topology);
        injectAddressInstance(address);
        cacheOnMessageMethods();
    }

    public boolean handleOnSpawn() {
        try {
            Method onSpawnMethod = getOnSpawnMethod();
            if (onSpawnMethod != null) {
                onSpawnMethod.setAccessible(true);
                onSpawnMethod.invoke(handler, new Object[0]);
            }
            return true;
        } catch (InvocationTargetException ex) {
            LOG.error(ex.getCause().getMessage(), ex.getCause());
            return false;
        } catch (Exception ex) {
            LOG.error("Error while invoking on spawn method for handler of type {}", handler.getClass());
            return false;
        }
    }

    public boolean handleOnSwap() {
        try {
            Method onSwapMethod = getOnSwapMethod();
            if (onSwapMethod != null) {
                onSwapMethod.setAccessible(true);
                onSwapMethod.invoke(handler, new Object[0]);
            }
            return true;
        } catch (InvocationTargetException ex) {
            LOG.error(ex.getCause().getMessage(), ex.getCause());
            return false;
        } catch (Exception ex) {
            LOG.error("Error while invoking on swap method for handler of type {}", handler.getClass());
            return false;
        }
    }

    public boolean handleOnRestart() {
        try {
            Method onRestartMethod = getOnRestartMethod();
            if (onRestartMethod != null) {
                onRestartMethod.setAccessible(true);
                onRestartMethod.invoke(handler, new Object[0]);
            }
            return true;
        } catch (InvocationTargetException ex) {
            LOG.error(ex.getCause().getMessage(), ex.getCause());
            return false;
        } catch (Exception ex) {
            LOG.error("Error while invoking on restart method for handler of type {}", handler.getClass());
            return false;
        }
    }

    public boolean handleOnKill() {
        try {
            Method onKillMethod = getOnKillMethod();
            if (onKillMethod != null) {
                onKillMethod.setAccessible(true);
                onKillMethod.invoke(handler, new Object[0]);
            }
            return true;
        } catch (InvocationTargetException ex) {
            LOG.error(ex.getCause().getMessage(), ex.getCause());
            return false;
        } catch (Exception ex) {
            LOG.error("Error while invoking on kill method for handler of type {}", handler.getClass());
            return false;
        }
    }

    public Object handleTrapExit(ExitActorMessage message) throws ActorExecutionException, KillActorException {
        Method trapExitMethod = getTrapExitMethod();
        if (trapExitMethod != null) {
            try {
                trapExitMethod.setAccessible(true);
                Object result = trapExitMethod.invoke(handler, message);
                return result;
            } catch (Exception ex) {
                if (ex instanceof InvocationTargetException && ex.getCause() instanceof KillActorException) {
                    LOG.warn("Killing actor on message of type {}", message.getClass());
                    throw (KillActorException) ex.getCause();
                } else if (ex instanceof InvocationTargetException) {
                    LOG.error(ex.getCause().getMessage(), ex.getCause());
                    throw new ActorExecutionException(ex.getCause().getMessage(), ex.getCause());
                } else {
                    LOG.error("Error while trapping exit on message of type {}", message.getClass());
                    throw new ActorExecutionException("Error while trapping exit on message of type " + message.getClass());
                }
            }
        } else {
            LOG.warn("Killing actor on message of type {}", message.getClass());
            throw new KillActorException();
        }
    }

    public Object handleOnMessage(Object message) throws ActorExecutionException, KillActorException {
        LOG.debug("Handler of type {} invoked on message: {}", handler.getClass(), message);
        Method invokable = getOnMessageMethodFor(message);
        if (invokable != null) {
            try {
                LOG.debug("Found method {} for message: {}", invokable, message);
                Class[] types = invokable.getParameterTypes();
                Object[] arguments;
                if (types.length == 1 && types[0].isAssignableFrom(message.getClass())) {
                    arguments = new Object[]{message};
                } else {
                    arguments = new Object[0];
                }
                invokable.setAccessible(true);
                Object result = invokable.invoke(handler, arguments);
                return result;
            } catch (Exception ex) {
                if (ex instanceof InvocationTargetException && ex.getCause() instanceof KillActorException) {
                    LOG.warn("Killing actor on message of type {}", message.getClass());
                    throw (KillActorException) ex.getCause();
                } else if (ex instanceof InvocationTargetException) {
                    LOG.error(ex.getCause().getMessage(), ex.getCause());
                    throw new ActorExecutionException(ex.getCause().getMessage(), ex.getCause());
                } else {
                    LOG.error("Error while executing handler of type {} on message of type {}", handler.getClass(), message.getClass());
                    throw new ActorExecutionException(ex.getMessage(), ex);
                }
            }
        } else {
            LOG.error("No suitable method for handler of type {} on message of type {}", handler.getClass(), message.getClass());
            throw new ActorExecutionException("No suitable method for handler of type " + handler.getClass() + " on message of type " + message.getClass());
        }
    }

    public RestartType getRestartType() {
        Restart restartAnnotation = handler.getClass().getAnnotation(Restart.class);
        if (restartAnnotation != null) {
            return restartAnnotation.type();
        } else {
            return RestartType.PERMANENT;
        }
    }

    private void injectAddressInstance(Address address) {
        try {
            Field[] fields = handler.getClass().getDeclaredFields();
            for (Field field : fields) {
                if (field.isAnnotationPresent(AddressInstance.class)) {
                    field.setAccessible(true);
                    field.set(handler, address);
                }
            }
        } catch (Exception ex) {
            LOG.warn("Unable to inject address instance.");
            LOG.warn(ex.getMessage(), ex);
        }
    }

    private void injectTopologyInstance(Topology topology) {
        try {
            Field[] fields = handler.getClass().getDeclaredFields();
            for (Field field : fields) {
                if (field.isAnnotationPresent(TopologyInstance.class)) {
                    field.setAccessible(true);
                    field.set(handler, topology);
                }
            }
        } catch (Exception ex) {
            LOG.warn("Unable to inject topology instance.");
            LOG.warn(ex.getMessage(), ex);
        }
    }

    private void cacheOnMessageMethods() {
        Method[] methods = handler.getClass().getMethods();
        for (Method method : methods) {
            OnMessage onMessage = method.getAnnotation(OnMessage.class);
            if (onMessage != null) {
                Class messageType = onMessage.type();
                onMessageMethods.put(messageType, method);
            }
        }
    }

    private Method getOnSpawnMethod() {
        Method[] methods = handler.getClass().getMethods();
        Method onSpawnMethod = null;
        for (Method method : methods) {
            if (method.isAnnotationPresent(OnSpawn.class) && method.getParameterTypes().length == 0) {
                onSpawnMethod = method;
            }
        }
        return onSpawnMethod;
    }

    private Method getOnSwapMethod() {
        Method[] methods = handler.getClass().getMethods();
        Method onSwapnMethod = null;
        for (Method method : methods) {
            if (method.isAnnotationPresent(OnSwap.class) && method.getParameterTypes().length == 0) {
                onSwapnMethod = method;
            }
        }
        return onSwapnMethod;
    }

    private Method getOnMessageMethodFor(Object message) {
        Method onMessageMethod = null;
        Class messageType = message.getClass();
        do {
            onMessageMethod = onMessageMethods.get(messageType);
            if (onMessageMethod == null) {
                messageType = messageType.getSuperclass();
            }
        } while (onMessageMethod == null && messageType != null);
        return onMessageMethod;
    }

    private Method getOnKillMethod() {
        Method[] methods = handler.getClass().getMethods();
        Method onKillMethod = null;
        for (Method method : methods) {
            if (method.isAnnotationPresent(OnKill.class) && method.getParameterTypes().length == 0) {
                onKillMethod = method;
            }
        }
        return onKillMethod;
    }

    private Method getOnRestartMethod() {
        Method[] methods = handler.getClass().getMethods();
        Method onRestartMethod = null;
        for (Method method : methods) {
            if (method.isAnnotationPresent(OnRestart.class) && method.getParameterTypes().length == 0) {
                onRestartMethod = method;
            }
        }
        return onRestartMethod;
    }

    private Method getTrapExitMethod() {
        Method[] methods = handler.getClass().getMethods();
        Method trapExitMethod = null;
        for (Method method : methods) {
            if (method.isAnnotationPresent(TrapExit.class) && method.getParameterTypes().length == 1 && method.getParameterTypes()[0].equals(ExitActorMessage.class)) {
                trapExitMethod = method;
            }
        }
        return trapExitMethod;
    }
}
